package util;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

import domain.BusinessObject;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.log4j.Logger;
import org.hibernate.collection.PersistentBag;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import vo.ValueObject;


@Component
public class PersistenceUtils {

        private static Logger log = Logger.getLogger(PersistenceUtils.class);

        /**
         * <p>
         * Instancia una <code>BusinessObject</code> en base a su <code>ValueObject</code> correspondiente.
         * Si el VO tiene como atributos otro ValueObject, crea los correspondientes <code>BusinessObjects</code>
         * </p>
         * <p>
         * Se basa en la convención de nombres siguiente:
         *
         *  Nombre              <- BusinessObject
         *  NombreVO    <- ValueObject
         *
         * </p>
         * @param vo
         * @param parent
     * @return
         */
        public BusinessObject build(ValueObject vo, Object parent) {
                // Instancio el BusinessObject
                BusinessObject bo= BeanUtils.instantiateClass(getBusinessObjectClass(vo));

                // Copio los atributos
                populateProperties(vo, bo, (parent==null ? bo : parent));

                // Retorno el BusinessObject
                return bo;
        }

    public BusinessObject merge(ValueObject vo, Object parent, BusinessObject bo) {

                // Copio los atributos
                populateProperties(vo, bo, (parent==null ? bo : parent));

                // Retorno el BusinessObject
                return bo;
        }

        /**
         * <p>
         * Instancia una <code>ValueObject</code> en base a su <code>BusinessObject</code> correspondiente.
         * Si el VO tiene como atributos otro BusinessObject, crea los correspondientes <code>ValueObject</code>
         * </p>
         * <p>
         * Se basa en la convención de nombres siguiente:
         *
         *  Nombre              <- BusinessObject
         *  NombreVO    <- ValueObject
         *
         * </p>
         * @param parent
     * @return
         */
        public ValueObject build(BusinessObject bo, Object parent) {
                // Instancio el BusinessObject
                ValueObject vo= BeanUtils.instantiateClass(getValueObjectClass(bo));

                // Copio los atributos
                populateProperties(bo, vo, parent);

                // Retorno el BusinessObject
                return vo;
        }

        /**
         * Retorna la clase de la entidad relacionada con el ValueObject.
         * Se debe respetar la convención de nombres siguiente:
         *
         *      Nombre          <- BusinessObject
         *  NombreVO    <- ValueObject
         *
         * @param vo
         * @return
         */
        @SuppressWarnings("unchecked")
        private Class<BusinessObject> getBusinessObjectClass(ValueObject vo) {
                String voClassName = vo.getClass().getName();
                String boClassName = voClassName.substring(0,voClassName.length()-2);
                boClassName = boClassName.replaceAll("vo", "domain");

                try {
                        return (Class<BusinessObject>) Class.forName(boClassName);
                } catch (ClassNotFoundException e) {
                        throw new RuntimeException("No se pudo cargar la clase: "+boClassName);
                }
        }

        /**
         * Retorna la clase del VO relacionado con el Entidad.
         * Se debe respetar la convención de nombres siguiente:            *
         * Nombre          <- BusinessObject            *
         * @return
         */
        @SuppressWarnings("unchecked")
        private Class<ValueObject> getValueObjectClass(BusinessObject bo) {
                String boClassName = bo.getClass().getName();
                String voClassName = boClassName.concat("VO");
                voClassName = voClassName.replaceAll("domain", "vo");
                try {
                        return (Class<ValueObject>) Class.forName(voClassName);
                } catch (ClassNotFoundException e) {
                        throw new RuntimeException("No se pudo cargar la clase: "+voClassName);
                }
        }



        /**
         * <p>
         * Popula las propiedades existentes en el objecto origen y las setea en el object destino.
         * En caso de no existir alguna propiedad en el destino, no se produce ningun error, solo se loguea
         * un Warning.
         * </p>
         * @param origen
     * @param destino
     * @param parent
     */
        public void populateProperties(Object origen, Object destino, Object parent) {


                /*
                 * obtengo las propiedades del ValueObject
                 */
                PropertyDescriptor[] propertiesDescriptorsO = BeanUtils.getPropertyDescriptors(origen.getClass());

                PropertyDescriptor[] propertiesDescriptorsD = BeanUtils.getPropertyDescriptors(destino.getClass());

                /*
                 * Itero las propiedades des objeto origen y las voy copiando al objecto destino
                 */
                for (PropertyDescriptor propertyDescriptor : propertiesDescriptorsO) {
                                copyProperty(propertyDescriptor, origen, destino,propertiesDescriptorsD, parent);
                }
        }

        /**
         * <p>
         * Copia las propiedades desde el objecto origen al destino en forma silenciosa; es decir no arroja ninguna excepcion
         * ante la inexistencia de algúna property en el origen o el destino.
         * </p>
         * <p>
         * Posee un tratamiento especial para los atributos de tipo <code>ValueObject</code> o <code>BusinessObject</code>.
         * En esos casos se reemplaza el valor de dicho atributo por el <code>BusinessObject</code> o el <code>ValueObject</code>
         * y se procede a utilizar este ultimo para valor a copiar en el destino.
         * Es decir:
         *      - Si el atributo es <code>ValueObject</code> -> construyo el <code>BusinessObject</code> correspondiente y lo seteo en el
         * mismo atributo del objecto destino.
         *  - Si el atributo es <code>BusinessObject</code> -> construyo el <code>ValueObject</code> correspondiente y lo seteo en el
         * mismo atributo del objecto destino.
         *  - Si el atributo es cualquier de cualquier otra clase, lo copia directamente en el mismo atributo del objecto destino.
         * </p>
         */
        private void copyProperty(PropertyDescriptor propertyDescriptor, Object origen, Object destino, PropertyDescriptor[] propertiesDescriptorsD, Object parent) {
                        // verifico que la propiedad exista en el destino
                        if (PropertyUtils.isWriteable(destino, propertyDescriptor.getName())) {

                Object value;
                if ((parent!=null) && parent.getClass().equals(getPropertyType(propertyDescriptor,propertiesDescriptorsD))) {
                     value = parent;
                } else {
                    value = readProperty(propertyDescriptor, origen);

                    if (value!=null) {
                        // tratamiento especial para BusinessObject y ValueObject
                        if (isValueObject(propertyDescriptor)) {
                            ValueObject voValue = (ValueObject)readProperty(propertyDescriptor, origen);
                            BusinessObject boValue = build(voValue, parent);
                            //populateProperties(voValue, boValue, boValue);
                            value = boValue;
                        } else if (isBusinessObject(propertyDescriptor)) {
                            BusinessObject boValue = (BusinessObject)readProperty(propertyDescriptor, origen);
                            ValueObject voValue = build(boValue, parent);
                            //populateProperties(boValue, voValue, voValue);
                            value = voValue;
                        } else if (isPersistentBag(value.getClass())) {
                            // es una lista de BusinessObject
                            List vos = new ArrayList();
                            if ( parent==null) {
                                for (Object bo : (PersistentBag)value ) {
                                    vos.add(build((BusinessObject)bo,destino));
                                }
                            }
                            value = vos;
                        } else if (isListOfValueObject(value.getClass())) {
                            List bos = new ArrayList();
                            for (Object vo : (List)value ) {
                                bos.add(build((ValueObject)vo, destino));
                            }
                            value = bos;
                        }

                    }

                }

                writeProperty(propertyDescriptor, destino, value, propertiesDescriptorsD);
            }
        }


        private boolean isValueObject(PropertyDescriptor pd) {
                return ValueObject.class.isAssignableFrom(pd.getPropertyType());
        }

    private boolean isListOfValueObject(Class clazz) {
                return List.class.isAssignableFrom(clazz);
        }

        private boolean isBusinessObject(PropertyDescriptor pd) {
                return BusinessObject.class.isAssignableFrom(pd.getPropertyType());
        }

    private boolean isPersistentBag(Class clazz) {
                return PersistentBag.class.isAssignableFrom(clazz);
        }

    private Class getPropertyType(PropertyDescriptor propertyDescriptor, PropertyDescriptor[] propertiesDescriptorsD) {
        PropertyDescriptor propertyDescriptorD = findPropertyDescriptor(propertyDescriptor.getName(), propertiesDescriptorsD);
        if (propertyDescriptorD!=null) {
            return propertyDescriptorD.getPropertyType();
        } else {
            return null;
        }
    }

        private void writeProperty(PropertyDescriptor propertyDescriptor, Object target, Object value, PropertyDescriptor[] propertiesDescriptorsD) {
                try {
                        PropertyDescriptor propertyDescriptorD = findPropertyDescriptor(propertyDescriptor.getName(), propertiesDescriptorsD);
                        if (propertyDescriptorD!=null) {
                                value = propertyDescriptorD.getWriteMethod().invoke(target, value);
                        }
                } catch (IllegalArgumentException e) {
                        log.error("No se pudo leer la propiedad: "+propertyDescriptor.getName(),e);
            e.printStackTrace();
                } catch (IllegalAccessException e) {
                        log.error("No se pudo leer la propiedad: "+propertyDescriptor.getName(),e);
            e.printStackTrace();
                } catch (InvocationTargetException e) {
                        log.error("No se pudo leer la propiedad: "+propertyDescriptor.getName(),e);
            e.printStackTrace();
                }
        }

        private PropertyDescriptor findPropertyDescriptor(String name, PropertyDescriptor[] propertiesDescriptorsD) {
                PropertyDescriptor propertyDescriptor=null;

                for (PropertyDescriptor pd : propertiesDescriptorsD) {
                        if (pd.getName().equals(name)) {
                                propertyDescriptor = pd;
                                break;
                        }
                }

                return propertyDescriptor;
        }

        private Object readProperty(PropertyDescriptor propertyDescriptor, Object target) {
                Object value=null;
                try {
                        value = propertyDescriptor.getReadMethod().invoke(target);
                } catch (IllegalArgumentException e) {
                        log.error("No se pudo leer la propiedad: "+propertyDescriptor.getName(),e);
                } catch (IllegalAccessException e) {
                        log.error("No se pudo leer la propiedad: "+propertyDescriptor.getName(),e);
                } catch (InvocationTargetException e) {
                        log.error("No se pudo leer la propiedad: "+propertyDescriptor.getName(),e);
                }
                return value;
        }


}